{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "# 基于模型的协同过滤算法\n",
    "\n",
    "本节介绍基于模型的协同过滤算法<sup><a id=\"fnr.1\" class=\"footref\" href=\"#fn.1\">1</a></sup>在Top-N推荐中的应用。\n",
    "\n",
    "核心思想是 **通过隐含特征（latent factor）联系用户兴趣和物品** 。\n",
    "\n",
    "思路：对于某个用户，首先得到其兴趣分类，然后从分类中挑选其可能喜欢的物品。\n",
    "\n",
    "上述基于兴趣分类的方法需要解决3个问题：\n",
    "\n",
    "1.  如何给物品进行分类？\n",
    "2.  如何确定用户对哪些类的物品感兴趣，以及感兴趣的程度？\n",
    "3.  对于一个给定的类，选择哪些属于这个类的物品推荐给用户，以及如何确定这些物品在一个类中的权重？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org613ebaf\"></a>\n",
    "## 基础算法\n",
    "\n",
    "不同于通过编辑给物品进行分类，而是从数据出发， **自动地给物品进行分类，** 然后进行个性化推荐。\n",
    "\n",
    "隐含语义分析技术采取 **基于用户行为统计的自动聚类** ，能较好地解决通过编辑进行分类的5种问题：\n",
    "\n",
    "-   编辑的意见不能代表各种用户的意见（分类从物品内容出发还是从用户出发）\n",
    "\n",
    "    隐含语义分析技术的分类来自对用户行为的统计，代表了用户对物品分类的看法。隐含语义分析技术和ItemCF在物品分类方面的思想类似， **如果两个物品被很多用户同时喜欢，那么这两个物品就很有可能属于同一个类。**\n",
    "\n",
    "-   编辑很难控制分类的粒度\n",
    "\n",
    "    隐含语义分析技术允许指定最终有多少个分类，这个数字越大，分类的粒度就会越细，反之分类粒度就越粗。\n",
    "\n",
    "-   编辑很难给一个物品多个分类\n",
    "\n",
    "    隐含语义分析技术会计算出物品属于每个类的权重，因此每个物品都不是硬性地被分到某一个类中。\n",
    "\n",
    "-   编辑很难给出多维度的分类\n",
    "\n",
    "    隐含语义分析技术给出的每个分类都不是同一个维度的，它是基于用户的共同兴趣计算出来的，如果用户的共同兴趣是某一个维度，那么LFM给出的类也是相同的维度。\n",
    "\n",
    "-   编辑很难决定一个物品在某一个分类中的权重\n",
    "\n",
    "    隐含语义分析技术可以通过统计用户行为决定物品在每个类中的权重，如果喜欢某个类的用户都会喜欢某个物品，那么这个物品在这个类中的权重就可能比较高。\n",
    "\n",
    "隐含语义分析技术有很多著名的模型和方法，其中和该技术相关且耳熟能详的名词有LFM（latent factor model）、pLSA、LDA、隐含类别模型（latent class model）、隐含主题模型（latent topic model）、 矩阵分解（matrix factorization）。这些技术和方法在本质上是相通的，其中很多方法都可以用于个性化推荐系统。\n",
    "\n",
    "下面将以 **矩阵分解** 介绍隐含语义分析技术在推荐系统中的应用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org054e63f\"></a>\n",
    "### 矩阵分解（Matrix Factorization, MF）\n",
    "\n",
    "假设用户——物品数据如下表所示：\n",
    "\n",
    "|      | D\\_1 | D\\_2 | D\\_3 | D\\_4 | D\\_5 |\n",
    "|------|------|------|------|------|------|\n",
    "| U\\_1 | 4    | 3    | -    | 5    | -    |\n",
    "| U\\_2 | 5    | -    | 4    | 4    | -    |\n",
    "| U\\_3 | 4    | -    | 5    | -    | 3    |\n",
    "| U\\_4 | 2    | 3    | -    | 1    | -    |\n",
    "| U\\_5 | -    | 4    | 2    | -    | 5    |\n",
    "\n",
    "将其转化为用户——物品矩阵 $R_{m \\times n}$ ，则：\n",
    "\n",
    "\\begin{equation}\n",
    "R_{m \\times n} =\n",
    "\\begin{pmatrix}\n",
    "  4 & 3 & - & 5 & -\\\\\n",
    "  5 & - & 4 & 4 & -\\\\\n",
    "  4 & - & 5 & - & 3\\\\\n",
    "  2 & 3 & - & 1 & -\\\\\n",
    "  - & 4 & 2 & - & 5\n",
    "\\end{pmatrix} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "其中“-”表示未打分项。\n",
    "\n",
    "矩阵分解是指将一个矩阵分解成两个或者多个矩阵的乘积。\n",
    "\n",
    "对于上面的用户——物品矩阵 $R_{m \\times n}$ ，假设将其分解成两个矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ ，要使得矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ 的乘积能够还原原始的矩阵 $R_{m \\times n}$ ：\n",
    "\n",
    "\\begin{equation}\n",
    "R_{m \\times n} \\approx P_{m \\times k} \\times Q_{k \\times n} = \\hat{R}_{m \\times n} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "其中，矩阵 $P_{m \\times k}$ 表示的是 $m \\times k$ 的矩阵，而矩阵 $Q_{k \\times n}$ 表示的是 $k \\times n$ 的矩阵， $k$ 是隐含的参数。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org32f67fe\"></a>\n",
    "### 基于矩阵分解的推荐算法\n",
    "\n",
    "矩阵分解算法属于基于模型的协同过滤算法，后者的步骤主要分为：\n",
    "\n",
    "1.  建立模型\n",
    "2.  利用训练好的模型进行推荐\n",
    "\n",
    "在基于矩阵分解的推荐算法中，上述两个步骤分别为：\n",
    "\n",
    "1.  对用户物品矩阵进行分解\n",
    "2.  利用分解后的矩阵预测原始矩阵中的未打分项\n",
    "\n",
    "通过如下公式计算用户 $i$ 对物品 $j$ 的兴趣：\n",
    "\n",
    "\\begin{equation}\n",
    "Preference(i,j) = r_{ij} = p_{i}^{T}q_{j} = \\underset{k=1}{\\overset{K}{\\sum}} p_{i,k}q_{k,j} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "$P_{i,k}$ 和 $q_{k,j}$ 是模型的参数， $p_{i,k}$ 度量用户 $i$ 的兴趣和第 $k$ 个隐类的关系， $q_{k,j}$ 度量了第 $k$ 个隐类和物品 $j$ 之间的关系。\n",
    "\n",
    "如何计算这两个参数？\n",
    "\n",
    "需要一个训练集，对于每个用户 $i$ ，训练集里包含了用户 $i$ 喜欢的物品和不感兴趣的物品，通过学习这个数据集，获得上面的模型参数。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org36b930e\"></a>\n",
    "#### 损失函数\n",
    "\n",
    "为了能够求解矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ 的每一个元素，可利用原始的评分矩阵 $R_{m \\times n}$ 与重新构建的评分矩阵 $\\hat{R}_{m \\times n}$ 之间的误差的平方作为损失函数，即：\n",
    "\n",
    "\\begin{equation}\n",
    "e^{2}_{i,j} = (r_{i,j} - \\hat{r}_{i,j})^{2} = (r_{i,j}-\\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})^{2} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "最终，需要求解所有的非“-”项的损失之和的最小值：\n",
    "\n",
    "\\begin{equation}\n",
    "\\min loss = \\underset{r_{i,j} \\neq -}{\\sum}{e_{i,j}^{2}} \\nonumber\n",
    "\\end{equation}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"orgab79781\"></a>\n",
    "#### 损失函数的求解\n",
    "\n",
    "对于上面平方损失函数的最小值，可以通过梯度下降法求解，梯度下降法核心步骤如下所示：\n",
    "\n",
    "-   求解损失函数的 **负梯度** <sup><a id=\"fnr.2\" class=\"footref\" href=\"#fn.2\">2</a></sup>：\n",
    "\n",
    "    \\begin{equation}\n",
    "    \\begin{split}\n",
    "    & \\frac{\\partial}{\\partial p_{i,k}}e^{2}_{i,j} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})q_{k,j} = -2e_{i,j}q_{k,j} \\\\\n",
    "    & \\frac{\\partial}{\\partial q_{k,j}}e^{2}_{i,j} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})p_{i,k} = -2e_{i,j}p_{i,k} \\\\\n",
    "    \\end{split} \\nonumber\n",
    "    \\end{equation}\n",
    "\n",
    "-   根据负梯度的方向更新变量：\n",
    "\n",
    "    \\begin{equation}\n",
    "    \\begin{split}\n",
    "    & p_{i,k}' = p_{i,k} - \\alpha \\frac{\\partial}{\\partial p_{i,k}}e^{2}_{i,j} = p_{i,k} + 2 \\alpha e_{i,j} q_{k,j} \\\\\n",
    "    & q_{k,j}' = q_{k,j} - \\alpha \\frac{\\partial}{\\partial q_{k,j}}e^{2}_{i,j} = q_{k,j} + 2 \\alpha e_{i,j} p_{i,k} \\\\\n",
    "    \\end{split} \\nonumber\n",
    "    \\end{equation}\n",
    "\n",
    "通过迭代，直到算法最终收敛。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"orgdf45ebc\"></a>\n",
    "#### 加入正则项的损失函数及求解方法\n",
    "\n",
    "通常在求解的过程中，为了能够有比较好的泛化能力，防止过拟合，会在损失函数中加入正则项，以对参数进行约束。下面是加入 $L_2$ 正则的损失函数：\n",
    "\n",
    "\\begin{equation}\n",
    "e_{i,j}^{2} = (r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})^{2} + \\frac{\\lambda}{2} \\overset{K}{\\underset{k=1}{\\sum}}(p_{i,k}^{2} + q_{k,j}^{2}) \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "正则化项的 $\\lambda$ 可通过实验获得。\n",
    "\n",
    "利用梯度下降法的求解过程为：\n",
    "\n",
    "-   求解损失函数的负梯度：\n",
    "\n",
    "    \\begin{equation}\n",
    "    \\begin{split}\n",
    "    & \\frac{\\partial}{\\partial p_{i,k}}e_{i,j}^{2} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}p_{i,k} \\cdot q_{k,j})q_{k,j} + \\lambda p_{i,k} = -2e_{i,j}q_{k,j} + \\lambda p_{i,k} \\\\\n",
    "    & \\frac{\\partial}{\\partial q_{k,j}}e_{i,j}^{2} = -2(r_{i,j} - \\overset{K}{\\underset{k=1}{\\sum}}q_{i,k} \\cdot q_{k,j})p_{i,k} + \\lambda q_{k,j} = -2e_{i,j}p_{i,k} + \\lambda q_{k,j} \\\\\n",
    "    \\end{split} \\nonumber\n",
    "    \\end{equation}\n",
    "\n",
    "-   根据负梯度的方向更新变量：\n",
    "\n",
    "    \\begin{equation}\n",
    "    \\begin{split}\n",
    "    & p_{i,k}' = p_{i,k} - \\alpha \\frac{\\partial}{\\partial p_{i,k}}e_{i,j}^{2} = p_{i,k} - \\alpha (-2e_{i,j}q_{k,j} + \\lambda p_{i,k}) = p_{i,k} + \\alpha (2e_{i,j}q_{k,j} - \\lambda p_{i,k}) \\\\\n",
    "    & q_{k,j}' = q_{k,j} - \\alpha \\frac{\\partial}{\\partial q_{k,j}}e_{i,j}^{2} = q_{k,j} - \\alpha (-2e_{i,j}p_{i,k} + \\lambda q_{k,j}) = q_{k,j} + \\alpha (2e_{i,j}p_{i,k} - \\lambda q_{k,j}) \\\\\n",
    "    \\end{split} \\nonumber\n",
    "    \\end{equation}\n",
    "\n",
    "通过迭代，直到算法最终收敛。\n",
    "\n",
    "其中 $\\alpha$ 是学习速率（learning rate），它的选取需要通过反复实验获得。\n",
    "\n",
    "```ipython\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "def sgd(data_matrix, k, alpha, lam, max_cycles):\n",
    "    \"\"\"使用梯度下降法进行矩阵分解。\n",
    "\n",
    "    Args:\n",
    "    - data_matrix: mat, 用户物品矩阵\n",
    "    - k: int, 分解矩阵的参数\n",
    "    - alpha: float, 学习率\n",
    "    - lam: float, 正则化参数\n",
    "    - max_cycles: int, 最大迭代次数\n",
    "\n",
    "    Returns:\n",
    "    p,q: mat, 分解后的矩阵\n",
    "    \"\"\"\n",
    "    m, n = np.shape(data_matrix)\n",
    "    # initiate p & q\n",
    "    p = np.mat(np.random.random((m, k)))\n",
    "    q = np.mat(np.random.random((k, n)))\n",
    "\n",
    "    # start training\n",
    "    for step in range(max_cycles):\n",
    "        for i in range(m):\n",
    "            for j in range(n):\n",
    "                if data_matrix[i, j] > 0:\n",
    "                    error = data_matrix[i, j]\n",
    "                    for r in range(k):\n",
    "                        error = error - p[i, r] * q[r, j]\n",
    "                    for r in range(k):\n",
    "                        p[i, r] = p[i, r] + alpha * (2 * error * q[r, j] - lam * p[i, r])\n",
    "                        q[r, j] = q[r, j] + alpha * (2 * error * p[i, r] - lam * q[r, j])\n",
    "\n",
    "        loss = 0.0\n",
    "        for i in range(m):\n",
    "            for j in range(n):\n",
    "                if data_matrix[i, j] > 0:\n",
    "                    error = 0.0\n",
    "                    for r in range(k):\n",
    "                        error = error + p[i, r] * q[r, j]\n",
    "                    # calculate loss function\n",
    "                    loss = (data_matrix[i, j] - error) * (data_matrix[i, j] - error)\n",
    "                    for r in range(k):\n",
    "                        loss = loss + lam * (p[i, r] * p[i, r] + q[r, j] * q[r, j]) / 2\n",
    "\n",
    "        if loss < 0.001:\n",
    "            break\n",
    "        if step % 1000 == 0:\n",
    "            print(\"\\titer: %d, loss: %f\" % (step, loss))\n",
    "    return p, q\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org1b3c5ef\"></a>\n",
    "#### 预测\n",
    "\n",
    "利用上述过程，可得到矩阵 $P_{m \\times k}$ 和 $Q_{k \\times n}$ ，模型便建立好了。\n",
    "\n",
    "在基于矩阵分解的推荐算法中需要为指定的用户进行推荐其未打分的项，若要计算用户 $i$ 对商品 $j$ 的打分，则计算方法为：\n",
    "\n",
    "\\begin{equation}\n",
    "\\overset{K}{\\underset{k=1}{\\sum}}p_{i,k}q_{k,j} \\nonumber\n",
    "\\end{equation}\n",
    "\n",
    "为用户预测的具体实现的程序如下所示：\n",
    "\n",
    "```ipython\n",
    "def prediction(data_matrix, p, q, user):\n",
    "    \"\"\"为用户未互动的项打分\n",
    "\n",
    "    Args:\n",
    "    - data_matrix: mat, 原始用户物品矩阵\n",
    "    - p: mat, 分解后的矩阵p\n",
    "    - q: mat, 分解后的矩阵q\n",
    "    - user: int, 用户的id\n",
    "\n",
    "    Returns:\n",
    "    - predict: list, 推荐列表\n",
    "    \"\"\"\n",
    "    n = np.shape(data_matrix)[1]\n",
    "    predict = {}\n",
    "    for j in range(n):\n",
    "        if data_matrix[user, j] == 0:\n",
    "            predict[j] = (p[user,] * q[:, j])[0, 0]\n",
    "\n",
    "    # 按照打分从大到小排序\n",
    "    return sorted(predict.items(), key=lambda d: d[1], reverse=True)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "<a id=\"org820e12d\"></a>\n",
    "\n",
    "## 隐语义模型和基于邻域的方法的比较\n",
    "\n",
    "LFM是一种基于机器学习的方法，具有比较好的理论基础。这个方法和基于邻域的方法(比如UserCF、ItemCF)相比，各有优缺点。\n",
    "\n",
    "-   理论基础\n",
    "\n",
    "    LFM具有比较好的理论基础，它是一种学习方法，通过优化一个设定的指标建立最优的模型。基于邻域的方法更多的是一种基于统计的方法，并没有学习过程。\n",
    "\n",
    "-   离线计算的空间复杂度\n",
    "\n",
    "    基于邻域的方法需要维护一张离线的相关表。在离线计算相关表的过程中，如果用户/物品数很多，将会占据很大的内存。假设有 $M$ 个用户和 $N$ 个物品，在计算相关表的过程中，可能会获得一张比较稠密的临时相关表（尽管最终对每个物品只保留 $K$ 个最相关的物品，但在中间计算过程中稠密的相关表是不可避免的），那么假设是用户相关表，则需要 $O(M \\times M)$ 的空间，而对于物品相关表，则需要 $O(N \\times N)$ 的空间。而LFM在建模过程中，如果是 $F$ 个隐类，那么它需要的存储空间是 $O(F \\times (M+N))$ ，这在 $M$ 和 $N$ 很大时可以很好地节省离线计算的内存。\n",
    "\n",
    "-   离线计算的时间复杂度\n",
    "\n",
    "    假设有 $M$ 个用户、 $N$ 个物品、 $K$ 条用户对物品的行为记录。那么，UserCF计算用户相关表的时间复杂度是 $O(N \\times (\\frac{K}{N})^2)$ ，而ItemCF计算物品相关表的时间复杂度是 $O(M \\times (\\frac{K}{M})^2)$ 。\n",
    "\n",
    "    而对于LFM，如果用 $F$ 个隐类，迭代 $S$ 次，那么它的计算复杂度是 $O(K \\times F \\times S)$ 。\n",
    "\n",
    "    如果 $\\frac{K}{N} > F \\times S$ ，则代表UserCF的时间复杂度低于LFM，如果 $\\frac{K}{M} > F \\times S$ ，则说明ItemCF的时间复杂度低于LFM。在一般情况下，LFM的时间复杂度要稍微高于UserCF和ItemCF，这主要是因为该算法需要多次迭代。但总体上，这两种算法在时间复杂度上没有质的差别。\n",
    "\n",
    "-   在线实时推荐\n",
    "\n",
    "    UserCF和ItemCF在线服务算法需要将相关表缓存在内存中，然后可以在线进行实时的预测。以ItemCF算法为例，一旦用户喜欢了新的物品，就可以通过查询内存中的相关表将和该物品相似的其他物品推荐给用户。因此，一旦用户有了新的行为，而且该行为被实时地记录到后台的数据库系统中，他的推荐列表就会发生变化。\n",
    "\n",
    "    而从LFM的预测公式可以看到，LFM在给用户生成推荐列表时，需要计算用户对所有物品的兴趣权重，然后排名，返回权重最大的 $N$ 个物品。那么，在物品数很多时，这一过程的时间复杂度非常高，可达 $O(M \\times N \\times F)$ 。因此，LFM不太适合用于物品数非常庞大的系统，如果要用，我们也需要一个比较快的算法给用户先计算一个比较小的候选列表，然后再用LFM重新排名。\n",
    "\n",
    "    另一方面，LFM在生成一个用户推荐列表时速度太慢，因此不能在线实时计算，而需要离线将所有用户的推荐结果事先计算好存储在数据库中。因此，LFM不能进行在线实时推荐，也就是说，当用户有了新的行为后，他的推荐列表不会发生变化。\n",
    "\n",
    "-   推荐解释\n",
    "\n",
    "    ItemCF算法支持很好的推荐解释，它可以利用用户的历史行为解释推荐结果。\n",
    "\n",
    "    但LFM无法提供这样的解释，它计算出的隐类虽然在语义上确实代表了一类兴趣和物品，却很难用自然语言描述并生成解释展现给用户。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "## 脚注\n",
    "\n",
    "<sup><a id=\"fn.1\" class=\"footnum\" href=\"#fnr.1\">1</a></sup> 相关的名词有LFM、LSI、pLSA、LDA、Topic Model和矩阵分解（matrix factorization）。\n",
    "\n",
    "<sup><a id=\"fn.2\" class=\"footnum\" href=\"#fnr.2\">2</a></sup> 梯度是一个向量，具有大小和方向。负梯度是函数值下降最快的方向。梯度下降是指沿着梯度下降的方向走。导数是梯度上升的方向，所以导数乘以负一是梯度下降方向，也就是说负导数决定你往哪个方向走。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "name": "7-recsys-lfm.ipynb"
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
